'use strict'

const fs = require('node:fs')

let fsp = fs.promises

let name_preg = /^[0-9]+$/

function getProcList() {
  let flist = fs.readdirSync('/proc', {withFileTypes: true})
  let rlist = []
  flist.filter(f => {
    if (f.isDirectory() && name_preg.test(f.name)) {
      if (f.name !== '1' && f.name !== '0') rlist.push(f.name)
    }
  })

  return rlist
}

let _stat_index = {
  pid     : 0,
  comm    : 1,
  state   : 2,
  ppid    : 3,
  pgrp    : 4,
  session : 5
}

/**
 * 进程目录中存在comm，保存命令的名称，cmdline保存完整命令格式
 * man 5 proc 参看stat部分
 * pid
 * comm 此部分表示命令名称，命令有可能会有空格，会使用()包含
 * 解析过程不能简单的使用空格分割
 */
function parse_linux_stat(pid) {
  try {
    let comm = fs.readFileSync(`/proc/${pid}/comm`, {encoding: 'utf8'})
    let stat = fs.readFileSync(`/proc/${pid}/stat`, {encoding: 'utf8'})
    let stlines = stat.replace(comm, '$').split(' ').filter(p => p.length > 0)
    let p = {}
    let val
    for (let k in _stat_index) {
      val = stlines[_stat_index[k]]
      if (name_preg.test(val)) {
        p[k] = parseInt(val)
      } else {
        p[k] = val
      }
    }

    p.comm = p.name = comm
    p.cmdline = fs.readFileSync(`/proc/${pid}/cmdline`, {encoding: 'utf8'})
    p.cmdsplit = p.cmdline.split('\x00').map(p => p.trim()).filter(p => p.length > 0)
    return p
  } catch (err) {
    return false
  }

  return false
}

function parse_ids(idtext, type = 'uid') {
  let ids = idtext.split(/\s+/).filter(p => p.length > 0)
  
  return [
    parseInt(ids[0]),
    parseInt(ids[1])
  ]
}

/**
 * 类Unix系列（主要是FreeBSD、OpenBSD等）没有/proc，似乎可以通过mount linprocfs /proc 进行兼容linux的挂载。
 * /proc/PID/status就是为了方便程序解析而提供的。
 */
async function async_parse_linux_status(pid, obj=null) {
  try {
    let stdata = await fsp.readFile(`/proc/${pid}/status`, {encoding: 'utf8'})
    let stlines = stdata.split('\n').map(d => {
      let rd = d.trim()
      let key_ind = d.indexOf(':')
      let key = d.substring(0, key_ind).toLowerCase()
      let val = d.substring(key_ind+1).trim()
      return [key, val]
    })

    let p = obj || {}
    
    let uids, gids

    stlines.forEach(st => {
      switch (st[0]) {
        case 'name':
          p.comm = st[1]
          p.name = st[1]
          break
        
        case 'vmrss':
          p.rss = parseInt(st[1])
          break

        case 'vmpte':
          p.pte = parseInt(st[1])
          break

        case 'pid':
        case 'ppid':
          p[st[0]] = parseInt(st[1])
          break

        case 'state':
          p[st[0]] = st[1]
          break

        case 'nssid':
          p.session = parseInt(st[1])
          break

        case 'nspgid':
          p.pgrp = parseInt(st[1])
          break

        case 'uid':
          uids = parse_ids(st[1])
          p.uid = uids[0]
          p.euid = uids[1]
          break

        case 'gid':
          gids = parse_ids(st[1])
          p.gid = gids[0]
          p.egid = gids[1]
          break
      }
    })

    let cmdline = await fsp.readFile(`/proc/${pid}/cmdline`, {encoding: 'utf8'})
    p.cmdline = cmdline
    p.cmdsplit = cmdline.split('\x00').map(p => p.trim()).filter(p => p.length > 0)
    return p
  } catch (err) {
    return false
  }

  return false
}

function parse_linux_status(pid) {
  try {
    let stdata = fs.readFileSync(`/proc/${pid}/status`, {encoding: 'utf8'})

    let stlines = stdata.split('\n').map(d => {
      let rd = d.trim()
      let key_ind = d.indexOf(':')
      let key = d.substring(0, key_ind).toLowerCase()
      let val = d.substring(key_ind+1).trim()
      return [key, val]
    })

    let p = {}

    let uids, gids

    stlines.forEach(st => {
      switch (st[0]) {
        case 'name':
          p.comm = st[1]
          p.name = st[1]
          break
        
        case 'vmrss':
          p.rss = parseInt(st[1])
          break

        case 'pid':
        case 'ppid':
          p[st[0]] = parseInt(st[1])
          break

        case 'state':
          p[st[0]] = st[1]
          break

        case 'nssid':
          p.session = parseInt(st[1])
          break

        case 'nspgid':
          p.pgrp = parseInt(st[1])
          break

        case 'uid':
          uids = parse_ids(st[1])
          p.uid = uids[0]
          p.euid = uids[1]
          break

        case 'gid':
          gids = parse_ids(st[1])
          p.gid = gids[0]
          p.egid = gids[1]
          break
      }
    })

    let cmdline = fs.readFileSync(`/proc/${pid}/cmdline`, {encoding: 'utf8'})
    p.cmdline = cmdline
    p.cmdsplit = cmdline.split('\x00').map(p => p.trim()).filter(p => p.length > 0)

    return p
  } catch (err) {
    return false
  }

  return false
}

/**
 *
 * 提供一个类似于ps命令的功能，输出结构提供进程列表，查询等功能。
 * 最主要的一个功能是直接提供进程树结构，并可以查找一个进程下所有子进程。
 *
 * */
function getAllChilds(pid, just_pid=false) {
    let childs = []
    let chs = []
    let pids = this.ptable[pid] || []
    let p

    while (true) {
      chs = []
      if (pids.length === 0) break

      for (let a of pids) {
        childs.push(just_pid ? a.pid : a)
        if (this.ptable[a.pid]) {
          chs.push(...this.ptable[a.pid])
        }
      }
      pids = chs
    }

    return childs
}

function isChild(ppid, pid) {
  if (!this.table[pid] || !this.table[ppid]) return false

  let p = this.table[pid]
  while (p && this.table[p.ppid]) {
    if (p.ppid === ppid) {
      return this.table[p.ppid] || true
    }

    p = this.table[p.ppid]
  }

  return false
}

function getChildsByCMDLine(cmdline) {
  let csplit = cmdline.split('\x00')

  let args = csplit.slice(1).sort((a, b) => {
    return a > b ? 1 : -1
  })

  let cline = csplit[0] + ' ' + args.join(' ')
  let cmds = []

  for (let k in this.table) {
    let p = this.table[k]
    let args = p.cmdsplit.slice(1).sort((a,b) => {
      return a > b ? 1 : -1
    })

    let p_cmdline = `${p.cmdsplit[0]} ${args.join(' ')}`

    if (p_cmdline === cline) {
      cmds.push(p)
    }
  }

  return cmds
}

/**
 * 根据名称和参数查找
 * @param {object} nametable
 * @param {array} args
 * */
function getChildsByCMDArgs(nametable, args) {
  let argstable = {}
  let count = args.length

  args.forEach(x => {
    argstable[x] = true
  })

  let cmds = []

  for (let k in this.table) {
    let p = this.table[k]
    if (!nametable[p.cmdsplit[0]]) continue

    let args = p.cmdsplit.slice(1)
    let ac = 0
    args.forEach(x => {
      argstable[x] && (ac++)
    })
    
    ;(count <= 0 || ac >= count) && cmds.push(p)
  }

  return cmds
}

function getChildsByCOMM(name) {
  let cmds = []
  for (let k in this.table) {
    let p = this.table[k]
    if (p.comm === name) {
      cmds.push(p)
    }
  }
  return cmds
}

function initProcessTreeData() {
  return {
      table: {},
      ptable: {},
      getAllChilds: getAllChilds,
      //检测pid是否为ppid的子进程
      isChild: isChild,
      getChildsByCMDLine: getChildsByCMDLine,
      getChildsByCOMM: getChildsByCOMM,
      getChildsByCMDArgs: getChildsByCMDArgs
  }
}

function nps() {
  let processData = initProcessTreeData()
  if (process.platform !== 'linux') return processData

  let rlist = getProcList()

  let p, cur

  for (let pid of rlist) {
    p = parse_linux_status(pid)
    if (!p) continue

    processData.table[pid] = p
    if (!processData.ptable[p.ppid]) {
      processData.ptable[p.ppid] = []
    }

    processData.ptable[p.ppid].push(p)
  }
  
  return processData
}

async function anps() {
  let processData = initProcessTreeData()
  if (process.platform !== 'linux') return processData

  let rlist = getProcList()

  let p, cur

  for (let pid of rlist) {
    p = await async_parse_linux_status(pid)
    if (!p) continue

    processData.table[pid] = p
    if (!processData.ptable[p.ppid]) {
      processData.ptable[p.ppid] = []
    }

    processData.ptable[p.ppid].push(p)
  }
  
  return processData
}

module.exports = {
  nps,
  anps,
  parse_linux_status,
  async_parse_linux_status
}
